﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Web;

namespace AjaxControlFramework.Reflection
{
    public delegate object PropertyGet(object instance);
    public delegate void PropertySet(object instance, object value);



    public static class PropertyCache
    {
        //------// Fields \\------------------------------------------------\\
        private static object InternalGetCollectionLock = new object();


        private static ReflectiveCacheCollection<PropertyGetCacheEntry> InternalGetCollection
        {
            get
            {
                ReflectiveCacheCollection<PropertyGetCacheEntry> collection = (ReflectiveCacheCollection<PropertyGetCacheEntry>)HttpContext.Current.Cache["AjaxControlFramework.Reflection.PropertyCache.InternalGetCollection"];

                if (collection == null)
                {
                    collection = new ReflectiveCacheCollection<PropertyGetCacheEntry>();
                    HttpContext.Current.Cache["AjaxControlFramework.Reflection.PropertyCache.InternalGetCollection"] = collection;
                }

                return collection;
            }
        }


        private static object InternalSetCollectionLock = new object();


        private static ReflectiveCacheCollection<PropertySetCacheEntry> InternalSetCollection
        {
            get
            {
                ReflectiveCacheCollection<PropertySetCacheEntry> collection = (ReflectiveCacheCollection<PropertySetCacheEntry>)HttpContext.Current.Cache["AjaxControlFramework.Reflection.PropertyCache.InternalSetCollection"];

                if (collection == null)
                {
                    collection = new ReflectiveCacheCollection<PropertySetCacheEntry>();
                    HttpContext.Current.Cache["AjaxControlFramework.Reflection.PropertyCache.InternalSetCollection"] = collection;
                }

                return collection;
            }
        }
        //------\\ Fields //------------------------------------------------//



        //------// Methods \\-----------------------------------------------\\
        /// <returns>Returns true if the property successfully resolved and a value was available, false otherwise.</returns>
        public static bool GetValue<T>(object instance, string propertyName, out object returnValue)
        {
            return GetValue(instance, typeof(T), propertyName, out returnValue);
        }


        /// <returns>Returns true if the property successfully resolved and was invoked, false otherwise.</returns>
        public static bool GetValue(object instance, Type instanceType, string propertyName, out object returnValue)
        {
            return GetValue(instance, instanceType, propertyName, out returnValue, null);
        }


        /// <returns>Returns true if the property successfully resolved and was invoked, false otherwise.</returns>
        public static bool GetValue(object instance, Type instanceType, string propertyName, out object returnValue, AuthenticationStrategy authenticationStrategy)
        {
            if (instanceType == null)
            {
                if (instance == null) { throw new ArgumentNullException("instanceType", "When the \"instance\" parameter is null, the property to be invoked is considered to be static. In these cases where \"instance\" is null, the \"instanceType\" parameter may not be null."); }
                else { throw new ArgumentNullException("instanceType"); }
            }


            PropertyGetCacheEntry cacheEntry = null;

            lock (InternalGetCollectionLock)
            {
                cacheEntry = InternalGetCollection[instanceType, propertyName];
            }

            if (cacheEntry != null)
            {
                if (cacheEntry.Delegate == null)
                {
                    returnValue = null;
                    return false;
                }
                else
                {
                    if (cacheEntry.Attribute.Protected && authenticationStrategy != null && (!authenticationStrategy.Authenticated || !authenticationStrategy.UserIsAuthorized(cacheEntry.Attribute.AuthorizedRoles)))
                    {
                        returnValue = null;
                        return false;
                    }
                    else
                    {
                        returnValue = cacheEntry.Delegate(instance);
                        return true;
                    }
                }
            }
            else
            {
                PropertyGet propertyGet = CachePropertyGetter(instanceType, propertyName);

                if (propertyGet == null)
                {
                    returnValue = null;
                    return false;
                }
                else
                {
                    if (cacheEntry.Attribute.Protected && authenticationStrategy != null && (!authenticationStrategy.Authenticated || !authenticationStrategy.UserIsAuthorized(cacheEntry.Attribute.AuthorizedRoles)))
                    {
                        returnValue = null;
                        return false;
                    }
                    else
                    {
                        returnValue = propertyGet(instance);
                        return true;
                    }
                }
            }
        }


        public static PropertyGet CachePropertyGetter(Type instanceType, string propertyName)
        {
            if (instanceType == null) { throw new ArgumentNullException("instanceType"); }

            PropertyInfo propertyInfo = instanceType.GetProperty(propertyName, BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
            if (propertyInfo == null) { throw new InvalidOperationException(String.Format("A property of the name {0} could not be found.", propertyName)); }

            AjaxControlPropertyAttribute propertyAttribute = (AjaxControlPropertyAttribute)propertyInfo.GetCustomAttributes(typeof(AjaxControlPropertyAttribute), true).FirstOrDefault();

            return CachePropertyGetter(instanceType, propertyAttribute, propertyInfo);
        }


        /// <summary>
        /// This property is meant for dynamic adding of AJAX Control Properties to the cache where the property may not exist at compile-time or, if it does exist, 
        /// may not have the AjaxControlPropertyAttribute assigned to it.
        /// </summary>
        /// <param name="instanceType"></param>
        /// <param name="actualPropertyName"></param>
        /// <param name="renderedPropertyName"></param>
        /// <returns></returns>
        public static PropertyGet CachePropertyGetter(Type instanceType, string actualPropertyName, string renderedPropertyName)
        {
            return CachePropertyGetter(instanceType, actualPropertyName, renderedPropertyName, null);
        }


        /// <summary>
        /// This property is meant for dynamic adding of AJAX Control Properties to the cache where the property may not exist at compile-time or, if it does exist, 
        /// may not have the AjaxControlPropertyAttribute assigned to it.
        /// </summary>
        /// <param name="instanceType"></param>
        /// <param name="actualPropertyName"></param>
        /// <param name="renderedPropertyName"></param>
        /// <returns></returns>
        public static PropertyGet CachePropertyGetter(Type instanceType, string actualPropertyName, string renderedPropertyName, object defaultValue)
        {
            if (instanceType == null) { throw new ArgumentNullException("instanceType"); }

            PropertyInfo propertyInfo = instanceType.GetProperty(actualPropertyName, BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
            if (propertyInfo == null) { throw new InvalidOperationException(String.Format("A property of the name {0} could not be found.", actualPropertyName)); }

            return CachePropertyGetter(instanceType, new AjaxControlPropertyAttribute(renderedPropertyName, defaultValue), propertyInfo);
        }


        public static PropertyGet CachePropertyGetter(Type instanceType, AjaxControlPropertyAttribute propertyAttribute, PropertyInfo propertyInfo)
        {
            if (instanceType == null) { throw new ArgumentNullException("instanceType"); }
            if (propertyAttribute == null) { throw new ArgumentNullException("propertyAttribute"); }
            if (propertyInfo == null) { throw new ArgumentNullException("propertyInfo"); }

            string propertyName = (propertyAttribute.Name == null || propertyAttribute.Name.Trim().Length == 0 ? propertyInfo.Name : propertyAttribute.Name.Trim());

            // Locking isn't going any further than this. Won't be checking again after the lock to see if the entry is STILL not in the 
            // internal collection as I want to allow for overwritting of cache entries.
            lock (InternalGetCollectionLock)
            {
                PropertyGetCacheEntry cacheEntry = new PropertyGetCacheEntry(propertyName, propertyAttribute);
                
                if (!propertyInfo.CanRead)
                {
                    InternalGetCollection[instanceType, propertyName] = cacheEntry;
                    return null;
                }

                MethodInfo propertyGetMethodInfo = propertyInfo.GetGetMethod(true);
                DynamicMethod dynamicMethod = new DynamicMethod(instanceType.Name + "_Get_" + propertyName, typeof(object), new Type[] { typeof(object) }, true);

                ILGenerator ilGenerator = dynamicMethod.GetILGenerator();

                ilGenerator.Emit(OpCodes.Ldarg_0);
                ilGenerator.Emit(OpCodes.Castclass, instanceType);

                if (propertyGetMethodInfo.IsVirtual)
                {
                    ilGenerator.Emit(OpCodes.Callvirt, propertyGetMethodInfo);
                }
                else
                {
                    ilGenerator.Emit(OpCodes.Call, propertyGetMethodInfo);
                }

                if (propertyInfo.PropertyType.IsValueType)
                {
                    ilGenerator.Emit(OpCodes.Box, propertyInfo.PropertyType);
                }
                else
                {
                    ilGenerator.Emit(OpCodes.Castclass, propertyInfo.PropertyType);
                }

                ilGenerator.Emit(OpCodes.Ret);

                PropertyGet propertyGet = (PropertyGet)dynamicMethod.CreateDelegate(typeof(PropertyGet));

                if (propertyGet == null)
                {
                    InternalGetCollection[instanceType, propertyName] = cacheEntry;
                    return null;
                }

                cacheEntry.Info = propertyInfo;
                cacheEntry.Delegate = propertyGet;

                InternalGetCollection[instanceType, propertyName] = cacheEntry;
                return propertyGet;
            }
        }


        /// <returns>Returns true if the property successfully resolved and a value was available, false otherwise.</returns>
        public static bool SetValue<T>(object instance, string propertyName, object value)
        {
            return SetValue(instance, typeof(T), propertyName, value);
        }


        /// <returns>Returns true if the property successfully resolved and was invoked, false otherwise.</returns>
        public static bool SetValue(object instance, Type instanceType, string propertyName, object value)
        {
            return SetValue(instance, instanceType, propertyName, value, null);
        }


        /// <returns>Returns true if the property successfully resolved and was invoked, false otherwise.</returns>
        public static bool SetValue(object instance, Type instanceType, string propertyName, object value, AuthenticationStrategy authenticationStrategy)
        {
            if (instanceType == null)
            {
                if (instance == null) { throw new ArgumentNullException("instanceType", "When the \"instance\" parameter is null, the property to be invoked is considered to be static. In these cases where \"instance\" is null, the \"instanceType\" parameter may not be null."); }
                else { throw new ArgumentNullException("instanceType"); }
            }


            PropertySetCacheEntry cacheEntry = null;

            lock (InternalSetCollectionLock)
            {
                cacheEntry = InternalSetCollection[instanceType, propertyName];
            }

            if (cacheEntry != null)
            {
                if (cacheEntry.Delegate == null)
                {
                    return false;
                }
                else
                {
                    if (cacheEntry.Attribute.Protected && authenticationStrategy != null && (!authenticationStrategy.Authenticated || !authenticationStrategy.UserIsAuthorized(cacheEntry.Attribute.AuthorizedRoles)))
                    {
                        return false;
                    }
                    else
                    {
                        cacheEntry.Delegate(instance, value);
                        return true;
                    }
                }
            }
            else
            {
                PropertySet propertySet = CachePropertySetter(instanceType, propertyName);

                if (propertySet == null)
                {
                    return false;
                }
                else
                {
                    if (cacheEntry.Attribute.Protected && authenticationStrategy != null && (!authenticationStrategy.Authenticated || !authenticationStrategy.UserIsAuthorized(cacheEntry.Attribute.AuthorizedRoles)))
                    {
                        return false;
                    }
                    else
                    {
                        propertySet(instance, value);
                        return true;
                    }
                }
            }
        }


        public static PropertySet CachePropertySetter(Type instanceType, string propertyName)
        {
            if (instanceType == null) { throw new ArgumentNullException("instanceType"); }

            PropertyInfo propertyInfo = instanceType.GetProperty(propertyName, BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
            if (propertyInfo == null) { throw new InvalidOperationException(String.Format("A property of the name {0} could not be found.", propertyName)); }

            AjaxControlPropertyAttribute propertyAttribute = (AjaxControlPropertyAttribute)propertyInfo.GetCustomAttributes(typeof(AjaxControlPropertyAttribute), true).FirstOrDefault();

            return CachePropertySetter(instanceType, propertyAttribute, propertyInfo);
        }


        /// <summary>
        /// This property is meant for dynamic adding of AJAX Control Properties to the cache where the property may not exist at compile-time or, if it does exist, 
        /// may not have the AjaxControlPropertyAttribute assigned to it.
        /// </summary>
        /// <param name="instanceType"></param>
        /// <param name="actualPropertyName"></param>
        /// <param name="renderedPropertyName"></param>
        /// <returns></returns>
        public static PropertySet CachePropertySetter(Type instanceType, string actualPropertyName, string renderedPropertyName)
        {
            return CachePropertySetter(instanceType, actualPropertyName, renderedPropertyName, null);
        }


        /// <summary>
        /// This property is meant for dynamic adding of AJAX Control Properties to the cache where the property may not exist at compile-time or, if it does exist, 
        /// may not have the AjaxControlPropertyAttribute assigned to it.
        /// </summary>
        /// <param name="instanceType"></param>
        /// <param name="actualPropertyName"></param>
        /// <param name="renderedPropertyName"></param>
        /// <returns></returns>
        public static PropertySet CachePropertySetter(Type instanceType, string actualPropertyName, string renderedPropertyName, object defaultValue)
        {
            if (instanceType == null) { throw new ArgumentNullException("instanceType"); }

            PropertyInfo propertyInfo = instanceType.GetProperty(actualPropertyName, BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
            if (propertyInfo == null) { throw new InvalidOperationException(String.Format("A property of the name {0} could not be found.", actualPropertyName)); }

            return CachePropertySetter(instanceType, new AjaxControlPropertyAttribute(renderedPropertyName, defaultValue), propertyInfo);
        }


        public static PropertySet CachePropertySetter(Type instanceType, AjaxControlPropertyAttribute propertyAttribute, PropertyInfo propertyInfo)
        {
            if (instanceType == null) { throw new ArgumentNullException("instanceType"); }
            if (propertyAttribute == null) { throw new ArgumentNullException("propertyAttribute"); }
            if (propertyInfo == null) { throw new ArgumentNullException("propertyInfo"); }
            
            string propertyName = (propertyAttribute.Name == null || propertyAttribute.Name.Trim().Length == 0 ? propertyInfo.Name : propertyAttribute.Name.Trim());


            // Locking isn't going any further than this. Won't be checking again after the lock to see if the entry is STILL not in the 
            // internal collection as I want to allow for overwritting of cache entries.
            lock (InternalGetCollectionLock)
            {
                PropertySetCacheEntry cacheEntry = new PropertySetCacheEntry(propertyName, propertyAttribute);

                if (!propertyInfo.CanWrite)
                {
                    InternalSetCollection[instanceType, propertyName] = cacheEntry;
                    return null;
                }

                MethodInfo propertySetMethodInfo = propertyInfo.GetSetMethod(true);
                DynamicMethod dynamicMethod = new DynamicMethod(instanceType.Name + "_Set_" + propertyName, typeof(void), new Type[] { typeof(object), typeof(object) }, true);

                ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
                ilGenerator.Emit(OpCodes.Ldarg_0);
                ilGenerator.Emit(OpCodes.Castclass, instanceType);
                ilGenerator.Emit(OpCodes.Ldarg_1);

                if (propertyInfo.PropertyType.IsValueType)
                {
                    if (propertyInfo.PropertyType.IsPrimitive)
                    {
                        ilGenerator.Emit(OpCodes.Call, typeof(Convert).GetMethod("To" + propertyInfo.PropertyType.Name, BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(object) }, null));
                    }
                    else
                    {
                        ilGenerator.Emit(OpCodes.Unbox_Any, propertyInfo.PropertyType);
                    }
                }
                else
                {
                    ilGenerator.Emit(OpCodes.Castclass, propertyInfo.PropertyType);
                }

                if (propertySetMethodInfo.IsVirtual)
                {
                    ilGenerator.Emit(OpCodes.Callvirt, propertySetMethodInfo);
                }
                else
                {
                    ilGenerator.Emit(OpCodes.Call, propertySetMethodInfo);
                }

                ilGenerator.Emit(OpCodes.Ret);

                PropertySet propertySet = (PropertySet)dynamicMethod.CreateDelegate(typeof(PropertySet));

                if (propertySet == null)
                {
                    InternalSetCollection[instanceType, propertyName] = cacheEntry;
                    return null;
                }

                cacheEntry.Info = propertyInfo;
                cacheEntry.Delegate = propertySet;

                InternalSetCollection[instanceType, propertyName] = cacheEntry;
                return propertySet;
            }
        }


        public static void CacheSetPropertiesOfType(Type instanceType)
        {
            PropertyInfo[] properties = instanceType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);

            foreach (PropertyInfo property in properties)
            {
                AjaxControlPropertyAttribute propertyAttribute = (AjaxControlPropertyAttribute)property.GetCustomAttributes(typeof(AjaxControlPropertyAttribute), true).FirstOrDefault();

                if (propertyAttribute != null)
                {
                    if (property.CanWrite)
                    {
                        CachePropertySetter(instanceType, propertyAttribute, property);
                    }
                }
            }
        }


        public static void CacheGetPropertiesOfType(Type instanceType)
        {
            PropertyInfo[] properties = instanceType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);

            foreach (PropertyInfo property in properties)
            {
                AjaxControlPropertyAttribute propertyAttribute = (AjaxControlPropertyAttribute)property.GetCustomAttributes(typeof(AjaxControlPropertyAttribute), true).FirstOrDefault();

                if (propertyAttribute != null)
                {
                    if (property.CanRead)
                    {
                        CachePropertyGetter(instanceType, propertyAttribute, property);
                    }
                }
            }
        }


        public static Dictionary<string, PropertyGetCacheEntry> GetPropertyGettersOfType(Type instanceType)
        {
            Dictionary<string, PropertyGetCacheEntry> getEntries = InternalGetCollection[instanceType];

            if (getEntries == null)
            {
                CacheGetPropertiesOfType(instanceType);
                getEntries = InternalGetCollection[instanceType];
            }

            if (getEntries == null) // If the collection is STILL null...
            {
                getEntries = new Dictionary<string, PropertyGetCacheEntry>(); // ...initialize the collection.
                InternalGetCollection[instanceType] = getEntries;
            }

            return getEntries;
        }


        public static Dictionary<string, PropertySetCacheEntry> GetPropertySettersOfType(Type instanceType)
        {
            Dictionary<string, PropertySetCacheEntry> setEntries = InternalSetCollection[instanceType];

            if (setEntries == null)
            {
                CacheSetPropertiesOfType(instanceType);
                setEntries = InternalSetCollection[instanceType];
            }

            if (setEntries == null) // If the collection is STILL null...
            {
                setEntries = new Dictionary<string, PropertySetCacheEntry>(); // ...initialize the collection.
                InternalSetCollection[instanceType] = setEntries;
            }

            return setEntries;
        }
        //------\\ Methods //-----------------------------------------------//
    }
}
